--
--  Copyright 2022 The Open Islands Authors. All Rights Reserved.
--
--  Licensed under the Apache License, Version 2.0 (the "License");
--  you may not use this file except in compliance with the License.
--  You may obtain a copy of the License at
--
--      http://www.apache.org/licenses/LICENSE-2.0
--
--  Unless required by applicable law or agreed to in writing, software
--  distributed under the License is distributed on an "AS IS" BASIS,
--  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
--  See the License for the specific language governing permissions and
--  limitations under the License.
--
local lrucache = require "resty.lrucache"
local ffi_lrucache = require "resty.lrucache.pureffi"
local yaml_parser = require("tinyyaml")
local string = require "string"
local utils = require "utils"
local apiserver = require "apiserver"
local config = require "config"
local constant = require("constant")
local dns_utils = require "dns_utils"
local log = require("log")

local io = io
local ngx = ngx
local new_timer = ngx.timer.at

local _M = {
    _VERSION = '0.1'
}

-- it can be shared by all the requests served by each nginx worker process:
local route_cache, err = lrucache.new(500)
if not route_cache then
    error("failed to create the route cache: " .. (err or "unknown"))
end

local function reload_route_table()
    _M.load_from_file()
end

function _M.load_from_file()
    -- not support now
    log.info("start load route table from file")
    local prefix_path = ngx.config.prefix()
    local route_table_config_path = prefix_path.."/conf/route_table.yaml"
    local file = io.open(route_table_config_path, "r")
    local content = file:read("*a")
    file:close()
    log.info(string.format("load route table config %s success", route_table_config_path))
    local yaml_table = yaml_parser.parse(content)
    for k, v in pairs(yaml_table) do
        route_cache:set(tostring(k), v)
    end
    log.info("load route table from file finish")
end

local function reload()
    reload_route_table()
    local ok, err = new_timer(60, reload)
    if not ok then
        if err ~= "process exiting" then
            errlog("failed to create timer: ", err)
        end
        reload_route_table()
        return
    end
end

function _M.crontab_reload()
    _M.load_from_file()
    local ok, err = new_timer(60, _M.crontab_reload)
    if not ok then
        if err ~= "process exiting" then
            log.error("failed to create timer: ")
        end
        _M.load_from_file()
        return
    end
end


function _M.update_route_file(route_table)
    -- todo
    -- route table must be yaml string
    local prefix = ngx.config.prefix()
    local route_table_path = prefix..ngx.var.ROUTE_TABLE_PATH
    local tmp_path = route_table_path..".tmp"
    local file = io.open(tmp_path, "w")
    -- try parse
    local yaml = yaml_parser.parse(route_table)
    if yaml then
        file:write(route_table)
        file:close()
    else
        return nil
    end
end

function _M.get_next_route(node_id)
    log.debug(string.format("try to get %s next address", node_id))
    local request_headers = ngx.req.get_headers()
    local proxy_jump_count = request_headers["Proxy-Jump-Count"]
    local err = "ok"
    if proxy_jump_count then
        proxy_jump_count = tonumber(proxy_jump_count)
        if proxy_jump_count and proxy_jump_count > 20 then
            err = "too much proxy jump count: ", proxy_jump_count
            log.error(err)
            return nil, nil, nil, err
        else
            ngx.req.set_header("Proxy-Jump-Count", tostring(proxy_jump_count + 1))
        end
    else
        ngx.req.set_header("Proxy-Jump-Count", "1")
    end
    local entry_addresses = _M.get_entry_addresses(node_id)
    if entry_addresses == nil or #entry_addresses == 0 then
        if node_id ~= "default" then
            log.info(node_id, " no address, start to get default route")
            return _M.get_next_route("default")
        else
            err = "no dest party route and no default proxy route"
            log.error(err)
            return nil, nil, nil, err
        end
    end

    local address, tls_enabled
    address, tls_enabled, err = _M.get_instance_address(entry_addresses)
    log.debug("get ", node_id, " next route, ", constant.FEDERATEDPROXY, ": ", address)
    return address, tls_enabled, constant.FEDERATEDPROXY, err
end

function _M.get_entry_addresses(node_id)
    if node_id ~= nil then
        local entry_addresses = route_cache:get(tostring(node_id))
        if entry_addresses then
            log.debug("query ", node_id, " entry addresses from route cache")
            return entry_addresses
        elseif config.node_id ~= "0" then
            entry_addresses = apiserver.query_route(node_id)
            if entry_addresses == nil or #entry_addresses == 0 then
                return nil
            end
            local ok, err = _M.addresses_resolution(entry_addresses)
            if not ok then
                log.error("failed to resolve the ip address of the entry:", err)
                return nil
            end
            log.info("get entry address from query")
            for i, address in pairs(entry_addresses) do
                for k, v in pairs(address) do
                    log.info(k, ": ", v)
                end
            end
            route_cache:set(tostring(node_id), entry_addresses, config.federatedproxy["route_cache_ttl"])
            log.info("set ", node_id, " entry addresses address into cache")
            return entry_addresses
        else
            log.info("no found ", node_id, " entry addresses from cache on party external mode")
        end
    else
        return nil
    end
end

function _M.get_route()
    return route_cache
end

function _M.addresses_resolution(addresses)
    local accessKeys = {httpAccess="", httpsAccess="", grpcAccess="", grpcsAccess=""}
    for i, address in pairs(addresses) do
        for k, v in pairs(address) do
            if accessKeys[k] ~= nil and type(address[k]) == "string" then
                local items = utils.string_split(address[k], ":")
                if items ~= nil then
                    if items[1] == nil or items[2] == nil then
                        return false, string.format("the value %s of %s is not valid", address[k], k)
                    end
                    local ip, err = dns_utils.get_ip_address(items[1])
                    if ip == nil then
                        return false, err
                    end
                    address[k] = ip..":"..items[2]
                end
            end
        end
    end
    return true, "ok"
end

function _M.get_instance_address(addresses)
    local address = addresses[math.random(1, #addresses)]
    local err = "ok"
    local protocol
    if ngx.req.http_version() == 2 then
        protocol = "grpc"
    else
        protocol = "http"
    end
    if address["tlsEnabled"] == true then
        protocol = protocol.."s"
    end
    return address[protocol.."Access"], address["tlsEnabled"], err
end

function _M.start()
    reload()
end

return _M
